﻿/**
 * Author: Junaid P V
 * License: New BSD License
 **/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;

namespace Org.AI.Neural
{
    /// <summary>
    /// Class that represent neurons in the neural network.
    /// It is basic element in the network.
    /// </summary>
    public class Neuron : IComparable<Neuron>
    {
        /// <summary>
        /// Unique id of neuron in the network
        /// </summary>
        private int id;

        /// <summary>
        /// List of neurons which this neuron's out put is connected to their inputs.
        /// </summary>
        private List<Neuron> outputNeurons=new List<Neuron>();

        /// <summary>
        /// Type of this neuron.
        /// </summary>
        private NeuronType type;

        // layer number for input layer it is 0 and output layer it is -1
        /// <summary>
        /// Layer number.
        /// For input layer neuron it is 0, output layer then it is -1
        /// </summary>
        private int layer;

        /// <summary>
        /// Inputs values for this neuron.
        /// in the case of input layer neurons weights should be initialized to 1,
        /// because network input not to be modified.
        /// For hidden and output layer neurons weights are initialized to real number
        /// between 0 and 1.
        /// </summary>
        private List<NeuronConnection> inputs=new List<NeuronConnection>();

        /// <summary>
        /// Save weights two level prior to the current weights
        /// </summary>
        private double[] weights2;

        /// <summary>
        /// Threshold it value.
        /// It is recommended to keep its value between 0.25 and 1
        /// </summary>
        private double threshold;

        /// <summary>
        /// Output value of this neuron.
        /// </summary>
        private double output;

        /// <summary>
        /// Error between desired output and current output.
        /// </summary>
        private double error;

        /// <summary>
        /// Desired output value.
        /// Set during the training.
        /// </summary>
        private double desiredOutput;

        private static Random random = new Random((int)DateTime.Now.Ticks);

        /// <summary>
        /// Set or get unique id of neuron
        /// </summary>
        public int Id
        {
            get { return this.id; }
            set { this.id = value; }
        }

        /// <summary>
        /// Get or set list of neurons those recieve this neuron's output as their input.
        /// </summary>
        public ReadOnlyCollection<Neuron> OutputNeurons
        {
            get { return new ReadOnlyCollection<Neuron>(this.outputNeurons); }
        }

        /// <summary>
        /// Get or set type of neuron.
        /// </summary>
        public NeuronType TypeOfNeuron
        {
            get { return this.type; }
            set { this.type = value; }
        }

        /// <summary>
        /// Get or set layer of this neuron.
        /// </summary>
        public int Layer
        {
            get { return this.layer; }
            set { this.layer = value; }
        }

        /// <summary>
        /// Get or set list of input connection to the neuron.
        /// </summary>
        public ReadOnlyCollection<NeuronConnection> InputConnections
        {
            get { return new ReadOnlyCollection<NeuronConnection>(this.inputs); }
        }

        /// <summary>
        /// Get or set the threshold value
        /// </summary>
        public double Threshold
        {
            get { return this.threshold; }
            set { this.threshold = value; }
        }

        /// <summary>
        /// Get output of the neuron.
        /// </summary>
        public double Output
        {
            get { return this.output; }
        }

        /// <summary>
        /// Get error in the output
        /// </summary>
        public double Error
        {
            get { return this.error; }
        }

        /// <summary>
        /// Get or set desired output value of the neuron.
        /// </summary>
        public double DesiredOutput
        {
            get { return this.desiredOutput; }
            set { this.desiredOutput = value; }
        }

        /// <summary>
        /// Construct a neuron which targeted to output layer
        /// </summary>
        public Neuron()
            :this(-1)
        {

        }

        /// <summary>
        /// Construct a neuron for a specified layer
        /// </summary>
        /// <param name="layer"></param>
        public Neuron(int layer)
        {
            this.layer=layer;
        }

        /// <summary>
        /// Construct a neuron for the specified layer with given id
        /// </summary>
        /// <param name="layer"></param>
        /// <param name="id"></param>
        public Neuron(int layer, int id)
        {
            this.layer = layer;
            this.id = id;
        }
        

        /// <summary>
        /// Initialize the neuron.
        /// by initializing weights and threshold
        /// </summary>
        public void Initialize()
        {
            this.InitWeights();
            this.threshold = (double)(Neuron.random.Next(25000, 100000)) / 100000.0f;
        }

        /// <summary>
        /// Calculate sigmoid value.
        /// </summary>
        /// <param name="variable">value to calculate the sigmoid</param>
        /// <returns>sigmoid value</returns>
        public static double Sigmoid(double variable)
        {
            return 1 / (1 + Math.Exp(-1 * variable));
        }

        /// <summary>
        /// String representation of the neuron
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            string str = "[ID: " + this.id + " Weights:{";
            foreach (NeuronConnection conn in this.inputs)
            {
                str += conn.Weight + ", ";
            }
            str += "} Threshold: " + this.threshold + "]";
            return str;
        }

        /// <summary>
        /// Initialization of weights
        /// </summary>
        private void InitWeights()
        {
            weights2 = new double[this.inputs.Count];
            if (this.layer==0)
            {
                for (int i = 0; i < this.inputs.Count; i++)
                {
                    // for input layer neuron weights are set to 1.
                    this.inputs[i].Weight=this.weights2[i] = 1;
                }
            }
            if (this.layer >0  || this.layer==-1)
            {
                for (int i = 0; i < this.inputs.Count; i++)
                {
                    // for hidden and output layer neurons it is set between 0 and 1.
                    this.inputs[i].Weight= this.weights2[i] = (double)(Neuron.random.Next(0, 100000)) / 100000;
                }
            }
        }

        /// <summary>
        /// Calculate output value of this neuron.
        /// </summary>
        public void CalculateOutput()
        {
            this.output = Neuron.Sigmoid(this.WeightedInputSum() * this.threshold);
        }

        /// <summary>
        /// Calculated the weighted sum of inputs.
        /// </summary>
        /// <returns>weighted sum of inputs</returns>
        private double WeightedInputSum()
        {
            double wSum = 0;
            foreach(NeuronConnection ic in this.inputs)
            {
                wSum += ic.Input * ic.Weight;
            }
            return wSum;
        }

        /// <summary>
        /// Calculate error for this neuron.
        /// Call this method for only output layer neurons.
        /// </summary>
        public void CalculateError()
        {
            if (this.layer == -1)
            {
                this.error = this.output * (1 - this.output) * (this.DesiredOutput - this.output);
            }
            else
            {
                double sumOfWeightedErrors = this.SumOfWeightedOutputErrors;
                this.error=this.output*(1-this.output)*sumOfWeightedErrors;
            }
        }

        
        /// <summary>
        /// Adjust weights of output layer neuron.
        /// </summary>
        /// <param name="learningRate">Learning rate</param>
        /// <param name="momentumFactor">Momentum factor</param>
        public void AdjustWeights(double learningRate,double momentumFactor)
        {
            // Place to store current weight values
            double[] weights1 = new double[this.inputs.Count];
            
            for (int i = 0; i < this.inputs.Count; i++)
            {
                // Saving current weights
                weights1[i] = this.inputs[i].Weight;
                this.inputs[i].Weight = weights1[i] + (1 - momentumFactor) * learningRate * this.error * this.inputs[i].Input + momentumFactor * (weights1[i] - weights2[i]);
                // saving prevous weights,
                // It is needed for calculation of weights next time
                this.weights2[i] = weights1[i];
            }
        }

        /// <summary>
        /// Adjust threshold of the neuron.
        /// </summary>
        /// <param name="learningRate">Learning rate</param>
        public void AdjustThreshold(double learningRate)
        {
            this.threshold += learningRate * this.error;
        }

        /// <summary>
        /// Add and connection from fromNeuron to this neuron
        /// </summary>
        /// <param name="fromNeuron">connection starting neuron</param>
        public void AddInputConnection(Neuron fromNeuron)
        {
            this.inputs.Add(new NeuronConnection(fromNeuron));
        }

        /// <summary>
        /// Connect this neuron and toNeuron by connecting this neuron's
        /// output to input of toNeuron.
        /// </summary>
        /// <param name="toNeuron"></param>
        public void AddOutputConnection(Neuron toNeuron)
        {
            this.outputNeurons.Add(toNeuron);
        }

        /// <summary>
        /// Feed the output of this neuron to the neuron connected to it
        /// through the copnnection.
        /// </summary>
        public void FeedOutputForward()
        {
            foreach (Neuron n in this.outputNeurons)
            {
                this.GetConnection(n).Input = this.output;
            }
        }

        /// <summary>
        /// Find input connection that is coming from given neuron
        /// </summary>
        /// <param name="fromNeuron">Neuron from which the connection is coming</param>
        /// <returns>input connection from the neuron</returns>
        public NeuronConnection GetConnection(Neuron fromNeuron)
        {
            return this.inputs.Find(new NeuralNetworkPredicates().SetId(fromNeuron.id).InputConnectionWithNeuron);
        }

        /// <summary>
        /// Helps to sort list of neurons
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public int CompareTo(Neuron other)
        {
            return id.CompareTo(other.id);
        }

        /// <summary>
        /// Get list of input connections in the neuron that is network input connections,
        /// not from other neurons.
        /// </summary>
        /// <returns>list of connections</returns>
        public ReadOnlyCollection<NeuronConnection> GetNetworkInputConnections
        {
            get
            {
                List<NeuronConnection> connections = new List<NeuronConnection>();
                foreach (NeuronConnection connection in this.inputs)
                {
                    if (connection.Neuron == null)
                    {
                        connections.Add(connection);
                    }
                }
                return new ReadOnlyCollection<NeuronConnection>(connections);
            }
        }

        
        /// <summary>
        /// Get Sum of product of weight of connections from this neurons and
        /// error at connection ending neurons
        /// </summary>
        /// <returns>Sum of weighted output error</returns>
        
        public double SumOfWeightedOutputErrors
        {
            get
            {
                double sumOfWeightedErrors = 0;
                foreach (Neuron neuron in this.outputNeurons)
                {
                    sumOfWeightedErrors += neuron.GetConnection(this).Weight * neuron.error;
                }
                return sumOfWeightedErrors;
            }
        }

        /// <summary>
        /// Set the input connections to this neuron
        /// </summary>
        /// <param name="numberOfInputs"></param>
        public void SetInputConnections(int numberOfInputs)
        {
            for (int i = 0; i < numberOfInputs; i++)
            {
                NeuronConnection connection = new NeuronConnection(null);
                inputs.Add(connection);
            }
        }

        /// <summary>
        /// Build and return XML element for the neuron
        /// </summary>
        /// <returns>XML element string</returns>
        public string ToXml()
        {
            StringBuilder xml = new StringBuilder(@"<neuron nid=""" + this.id + @""" >");
            xml.Append("<layer>" + this.layer + "</layer>");
            xml.Append("<threshold>" + this.threshold + "</threshold>");
            xml.Append("<function></function>");
            xml.Append("<inputs>");
            foreach (NeuronConnection connection in this.inputs)
            {
                xml.Append("<weight>" + connection.Weight + "</weight>");
            }
            xml.Append("</inputs>");
            xml.Append("</neuron>");
            return xml.ToString();
        }

    }
}
